/*
 * @(#) $Header$
 *
 * Copyright (C) 2007  Forklabs Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.media.jai.operator;

import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import java.util.Arrays;
import javax.media.jai.JAI;
import javax.media.jai.OperationDescriptor;
import javax.media.jai.OperationDescriptorImpl;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.RenderedOp;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.registry.RenderedRegistryMode;
import javax.media.jai.util.Range;
import ca.forklabs.baselib.util.BinaryFunction;

/**
 * Class {@code KMeansDescriptor} is an {@link OperationDescriptor} describing
 * the <em>kmeans</em> operation. This operation performs applies the
 * <a href="http://en.wikipedia.org/wiki/K-means_algorithm">k-means</a> on the
 * given source images.
 * <p>
 * The <em>kmeans</em> operation takes two parameter: - the number of cluster,
 * - and optional evaluation function (defaults to taking the color components
 * for the vector to partition), - an optional number of iteration (defaults to
 * <em>100</em>) and - an optional color map (defaults to the center of the
 * cluster).
 *
 * <table border=1>
 * <caption>Resource List</caption>
 * <tr><th>Name</th>        <th>Value</th></tr>
 * <tr><td>GlobalName</td>  <td>kmeans</td></tr>
 * <tr><td>LocalName</td>   <td>kmeans</td></tr>
 * <tr><td>Vendor</td>      <td>ca.forklabs.media.jai.opimage</td></tr>
 * <tr><td>Description</td> <td>K-means algorithm</td></tr>
 * <tr><td>DocURL</td>      <td>n/a</td></tr>
 * <tr><td>Version</td>     <td>$Version$</td></tr>
 * <tr><td>Arg0Desct</td>   <td>The number of clusters</td></tr>
 * <tr><td>Arg1Desct</td>   <td>The evaluation function (optional)</td></tr>
 * <tr><td>Arg2Desct</td>   <td>The maximum number of iteration (optional)</td></tr>
 * <tr><td>Arg3Desct</td>   <td>A new color map (optional)</td></tr>
 * </table>
 *
 * <table border=1>
 * <caption>Parameter List</caption>
 * <tr><th>Name</th>         <th>Class Type</th>                            <th>Default Value</th></tr>
 * <tr><td>clusters</td>     <td>Integer</td>                               <td>NO_PARAMETER_DEFAULT</td>
 * <tr><td>function</td>     <td>KMeansDescriptor.EvaluationFunction</td>   <td>new KMeansDescriptor.ColorEvaluationFunction</td>
 * <tr><td>iterations</td>   <td>Integer</td>                               <td>100</td>
 * <tr><td>colormap</td>     <td>int[][]</td>                               <td>the cluster order</td>
 * </table>
 *
 * This operators works in {@code rendered} mode only.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.media.jai.operator.KMeansDescriptor">Daniel Léonard</a>
 * @version $Revision$
 */
public class KMeansDescriptor extends OperationDescriptorImpl {

//---------------------------
// Inner class
//---------------------------

   /**
    * Interface {@code EvaluationFunction} represents an evaluation function
    * that gives a vector to partition for the given pixel.
    *
    * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.media.jai.operator.KMeansDescriptor$EvaluationFunction">Daniel Léonard</a>
    * @version $Revision: 1.3 $
    */
   public static interface EvaluationFunction extends BinaryFunction<double[], RandomIter, Point> {

      /**
       * Gives the vector to clusterize. Values that must be retained between
       * calls to this method are copied or cloned by the implementation,
       * meaning that the same array can be returned call after call.
       * @param   iterator   a random iterator on all the pixels of the source
       *                     image.
       * @param   position   the coordinate of the pixel.
       * @return   the vector.
       */
      public double[] invoke(RandomIter iterator, Point position);

      }

   /**
    * Class {@code ColorEvaluationFunction} extracts the color components and
    * gives them as the vector to partition.
    *
    * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.media.jai.operator.KMeansDescriptor$ColorEvaluationFunction">Daniel Léonard</a>
    * @version $Revision: 1.3 $
    */
   public static class ColorEvaluationFunction implements EvaluationFunction {
      /** The array to extract the pixel. */
      private double[] color = null;

      /**
       * Extracts the color component of the given pixel and gives them as the
       * vector to partition.
       * @param   iterator   a random iterator on all the pixels of the source
       *                     image.
       * @param   position   the coordinate of the pixel.
       * @return   the vector.
       */
      @Override
      public double[] invoke(RandomIter iterator, Point position) {
         int x = position.x;
         int y = position.y;

         this.color = iterator.getPixel(x, y, this.color);

         return this.color;
         }

      }



//---------------------------
// Class variables
//---------------------------

   /** <em>serialVersionUID</em>. */
   private static final long serialVersionUID = 7829097636253278365L;

   /** The name of this operator. */
   public static final String NAME = "KMeans"; //$NON-NLS-1$

   /** The name of the number of cluster parameter. */
   public static final String CLUSTER_PARAMETER_NAME = "clusters"; //$NON-NLS-1$
   /** The name of the number of evaluation function parameter. */
   public static final String EVALUATION_FUNCTION_PARAMETER_NAME = "evaluationFunction"; //$NON-NLS-1$
   /** The name of the number of iterations parameter. */
   public static final String ITERATIONS_PARAMETER_NAME = "iterations"; //$NON-NLS-1$
   /** The name of the color map parameter. */
   public static final String COLOR_MAP_PARAMETER_NAME = "colormap"; //$NON-NLS-1$

   /** The index in the parameter block of the number of cluster parameter. */
   public static final int CLUSTER_PARAMETER_INDEX = 0;
   /** The index in the parameter block of the evaluation function parameter. */
   public static final int EVALUATION_FUNCTION_PARAMETER_INDEX = 1;
   /** The index in the parameter block of the iterations parameter. */
   public static final int ITERATIONS_PARAMETER_INDEX = 2;
   /** The index in the parameter block of the color map parameter. */
   public static final int COLOR_MAP_PARAMETER_INDEX = 3;

   /** The default value for the number of iterations, arbitrarily <em>100</em>. */
   public static final int ITERATIONS_DEFAULT_VALUE = 100;

   /**
    * The resource strings that provide the general documentation and specify
    * the parameter list for this operation.
    */
   private static final String[][] RESOURCES =
   {
      { "GlobalName",  KMeansDescriptor.NAME, }, //$NON-NLS-1$
      { "LocalName",   KMeansDescriptor.NAME, }, //$NON-NLS-1$
      { "Vendor",      "ca.forklabs.media.jai.opimage", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "Description", KMeansDescriptor.getDescription(), }, //$NON-NLS-1$
      { "DocURL",      "n/a", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "Version",     "$Version$", }, //$NON-NLS-1$ //$NON-NLS-2$
      { "arg0Desc",    KMeansDescriptor.getArg0Description(), }, //$NON-NLS-1$
      { "arg1Desc",    KMeansDescriptor.getArg1Description(), }, //$NON-NLS-1$
      { "arg2Desc",    KMeansDescriptor.getArg2Description(), }, //$NON-NLS-1$
      { "arg3Desc",    KMeansDescriptor.getArg3Description(), }, //$NON-NLS-1$
   };

   /** The supported modes. */
   private static final String[] SUPPORTED_MODES =
   {
      RenderedRegistryMode.MODE_NAME,
   };

   /** The parameter class list for this operation. */
   private static final Class<?>[] PARAMETER_CLASSES = new Class<?>[]
   {
      Integer.class,
      EvaluationFunction.class,
      Integer.class,
      int[][].class,
   };

   /** The parameter name list for this operation. */
   private static final String[] PARAMETER_NAMES = new String[]
   {
      KMeansDescriptor.CLUSTER_PARAMETER_NAME,
      KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_NAME,
      KMeansDescriptor.ITERATIONS_PARAMETER_NAME,
      KMeansDescriptor.COLOR_MAP_PARAMETER_NAME,
   };

   /** The default parameters. */
   @SuppressWarnings({ "boxing", "static-access" })
   private static final Object[] PARAMETER_DEFAULTS = new Object[]
   {
      KMeansDescriptor.NO_PARAMETER_DEFAULT,
      null,
      KMeansDescriptor.ITERATIONS_DEFAULT_VALUE,
      null,
   };

   /** Valid parameter values. */
   @SuppressWarnings("boxing")
   private static final Object[] VALID_PARAMETERS = new Object[]
   {
      new Range(Integer.class, 1, Integer.MAX_VALUE),
      null,
      new Range(Integer.class, 1, Integer.MAX_VALUE),
      null,
   };


//---------------------------
// Constructor
//---------------------------

   /**
    * Constructor.
    */
   public KMeansDescriptor() {
      super(RESOURCES, SUPPORTED_MODES, 1, PARAMETER_NAMES, PARAMETER_CLASSES, PARAMETER_DEFAULTS, VALID_PARAMETERS);
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Creates the default color map.
    * @param   clusters   the number of clusters.
    * @param   bands   the number of bands.
    * @return   the default color map.
    */
   protected int[][] createDefaultColorMap(int clusters, int bands) {
      int[][] color_map = new int[clusters][bands];
      for (int c = 0; c < clusters; c++) {
         Arrays.fill(color_map[c], c);
         }
      return color_map;
      }

   /**
    * Validates the evaluation function. That is, put the default evaluation
    * function in the parameter block if the value of the parameter is
    * {@code null}.
    * @param   mode   ignored.
    * @param   pb   the parameter block.
    * @param   sb   ignored.
    * @return   always {@code true}.
    */
   protected boolean validateEvaluationFunction(String mode, ParameterBlock pb, StringBuffer sb) {
   // if no evaluation function is supplied,
   // put the default evaluation function
      EvaluationFunction function = (EvaluationFunction) pb.getObjectParameter(KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_INDEX);
      if (null == function) {
         function = new ColorEvaluationFunction();
         pb.set(function, KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_INDEX);
         }
      return true;
      }

   /**
    * Validates the color map. It checks that the color map has the same number
    * of color as the number of clusters and that each color has the same number
    * of components as the source image.
    * @param   mode   ignored.
    * @param   pb   the parameter block.
    * @param   sb   the error message buffer.
    * @return   {@code true} if the color map is correct,
    *           {@code false} otherwise.
    */
   protected boolean validateColorMap(String mode, ParameterBlock pb, StringBuffer sb) {
      boolean all_ok = true;

      int expected_clusters = pb.getIntParameter(KMeansDescriptor.CLUSTER_PARAMETER_INDEX);

      RenderedImage source = (RenderedImage) pb.getSource(0);
      SampleModel sample_model = source.getSampleModel();
      int expected_bands = sample_model.getNumBands();

   // if no color map exist, create
   // the default color map
      int[][] color_map = (int[][]) pb.getObjectParameter(KMeansDescriptor.COLOR_MAP_PARAMETER_INDEX);
      if (null == color_map) {
         color_map = this.createDefaultColorMap(expected_clusters, expected_bands);
         pb.set(color_map, KMeansDescriptor.COLOR_MAP_PARAMETER_INDEX);
         }

   // check that the number of color
   // in the color map matches the
   // number of cluster: one color per cluster
      int color_map_clusters = color_map.length;
      all_ok = (expected_clusters == color_map_clusters);
      if (false == all_ok) {
         String message = this.getClustersDoNotMatchErrorMessage(expected_clusters, color_map_clusters);
         sb.append(message);
         return all_ok;
         }

   // check that each color in the color
   // map have the same number of components
      int color_map_bands = color_map[0].length;
      for (int i = 1, len = color_map.length; i < len; i++) {
         all_ok = (color_map[i].length == color_map_bands);
         if (false == all_ok) {
            String message = this.getColorMapNotRectangularErrorMessage(color_map_bands, color_map[i].length);
            sb.append(message);
            return all_ok;
            }
         }

   // and finally, check that the color
   // have the expected number of components
      all_ok = (expected_bands == color_map_bands);
      if (false == all_ok) {
         String message = this.getBandsDoNotMatchErrorMessage(expected_bands, color_map_bands);
         sb.append(message);
         return all_ok;
         }

      return all_ok;
      }


//---------------------------
// Overridden methods from javax.media.jai.OperationDescriptorImpl
//---------------------------

   /**
    * Validates that the <em>colormap</em> parameter matches the number of bands
    * in the image and the number of clusters.
    * @param   mode   the rendering mode.
    * @param   pb   the parameter block.
    * @param   sb   the error message buffer.
    * @return   {@code true} if all is OK, {@code false} otherwise.
    */
   @Override
   public boolean validateParameters(String mode, ParameterBlock pb, StringBuffer sb) {
      boolean all_ok = super.validateParameters(mode, pb, sb);
      if (false == all_ok) {
         return all_ok;
         }

      all_ok = this.validateEvaluationFunction(mode, pb, sb);
      if (false == all_ok) {
         return all_ok;
         }

      all_ok = this.validateColorMap(mode, pb, sb);
      if (false == all_ok) {
         return all_ok;
         }

      return all_ok;
      }


//---------------------------
// Class methods
//---------------------------

   /**
    * Creates and fills a new parameter block.
    * @param   mode   the rendering mode.
    * @param   source   the source image.
    * @param   clusters   the number of clusters.
    * @param   function   the evaluation function.
    * @param   iterations   the maximum number of iterations (<em>0</em> will
    *                       use the default value).
    * @param   colormap   the color map ({@code null} will use the default color
    *                     map).
    * @return   a new parameter block.
    */
   public static ParameterBlockJAI createParameterBlock(String mode, Object source, int clusters, KMeansDescriptor.EvaluationFunction function, int iterations, int[][] colormap) {
      String name = KMeansDescriptor.NAME;
      ParameterBlockJAI pb = new ParameterBlockJAI(name, mode);
      if (null != source) {
         pb.addSource(source);
         }
      pb.setParameter(KMeansDescriptor.CLUSTER_PARAMETER_NAME, clusters);
      pb.setParameter(KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_NAME, function);
      if (0 != iterations) { pb.setParameter(KMeansDescriptor.ITERATIONS_PARAMETER_NAME, iterations); }
      if (null != colormap) { pb.setParameter(KMeansDescriptor.COLOR_MAP_PARAMETER_NAME, colormap); }
      return pb;
      }

   /**
    * Creates a rendered image.
    * @param   source   the source image.
    * @param   clusters   the number of clusters.
    * @param   function   the evaluation function.
    * @param   iterations   the maximum number of iterations (<em>0</em> will
    *                       use the default value).
    * @param   colormap   the color map ({@code null} will use the default color
    *                     map).
    * @param   hints    the rendering hints, may be {@code null}.
    * @return   the new image.
    */
   public static RenderedOp create(RenderedImage source, int clusters, KMeansDescriptor.EvaluationFunction function, int iterations, int[][] colormap, RenderingHints hints) {
      String operation = KMeansDescriptor.NAME;
      String mode = RenderedRegistryMode.MODE_NAME;
      ParameterBlockJAI pb = KMeansDescriptor.createParameterBlock(mode, source, clusters, function, iterations, colormap);
      RenderedOp image = JAI.create(operation, pb, hints);
      return image;
      }


//---------------------------
// External resources methods
//---------------------------

   /**
    * Gets the description of this operation.
    * @return   the description message.
    */
   protected static String getDescription() {
      String key = Resources.KMEANS_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the first argument, the number of clusters.
    * @return   the description message.
    */
   protected static String getArg0Description() {
      String key = Resources.KMEANS_ARG0_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the second argument, the evaluation function.
    * @return   the description message.
    */
   protected static String getArg1Description() {
      String key = Resources.KMEANS_ARG1_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the third argument, the maximum number of
    * iterations.
    * @return   the description message.
    */
   protected static String getArg2Description() {
      String key = Resources.KMEANS_ARG2_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the description for the fourth argument, the color map.
    * @return   the description message.
    */
   protected static String getArg3Description() {
      String key = Resources.KMEANS_ARG3_DESCRIPTION;
      String message = Resources.getLocalizedString(key);
      return message;
      }

   /**
    * Gets the error message telling that the number of clusters in the color
    * map do not match the number of clusters.
    * @param   expected   the number of clusters.
    * @param   got   the number of clusters in the color map.
    * @return   the error message.
    */
   @SuppressWarnings("boxing")
   protected String getClustersDoNotMatchErrorMessage(int expected, int got) {
      String key = Resources.KMEANS_BAD_CLUSTERS;
      String message = Resources.getLocalizedString(key, expected, got);
      return message;
      }

   /**
    * Gets the error message telling that not all colors in the color map have
    * the same number of bands.
    * @param   expected   one number of bands.
    * @param   got   another number of bands.
    * @return   the error message.
    */
   @SuppressWarnings("boxing")
   protected String getColorMapNotRectangularErrorMessage(int expected, int got) {
      String key = Resources.KMEANS_COLOR_MAP_NOT_RECTANGULAR;
      String message = Resources.getLocalizedString(key, expected, got);
      return message;
      }

   /**
    * Gets the error message telling that the number of bands in the color
    * map do not match the number of bands in the source image.
    * @param   expected   the number of bands in the image.
    * @param   got   the number of bands in the color map.
    * @return   the error message.
    */
   @SuppressWarnings("boxing")
   protected String getBandsDoNotMatchErrorMessage(int expected, int got) {
      String key = Resources.KMEANS_BAD_BANDS;
      String message = Resources.getLocalizedString(key, expected, got);
      return message;
      }

   }
